import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.IntStream;

public class HashMapBenchmark {

    public static class Value {
        public Long aLong;
        public BigDecimal decimal;
        public String string;

        @Override
        public int hashCode() {
            return this.aLong.hashCode() ^ this.decimal.hashCode() ^ this.string.hashCode();
        }
    }

    public static void main(String[] args) {
        printMemory("start");

        boolean forcedGc = true;
        if (args.length > 0) {
            forcedGc = Boolean.parseBoolean(args[0]);
        }

        int numElements = 2_000_000;
        if (args.length > 1) {
            numElements = Integer.parseInt(args[1]);
        }

        long putStartMillis = System.currentTimeMillis();
        Map<String, Value> map = new ConcurrentHashMap<>(numElements);
        int expectedHashCode = IntStream.range(0, numElements).parallel()
                .map((i) -> {
                    ThreadLocalRandom random = ThreadLocalRandom.current();
                    String key = Long.toString(random.nextLong());
                    Value value = new Value();
                    value.aLong = random.nextLong();
                    value.decimal = BigDecimal.valueOf(random.nextLong(), random.nextInt());
                    value.string = Long.toString(random.nextLong());
                    map.put(key, value);
                    return key.hashCode() ^ value.hashCode();
                }).reduce(0, (a, b) -> a ^ b);
        long putEndMillis = System.currentTimeMillis();
        printElapsedTime("put", putStartMillis, putEndMillis);

        if (forcedGc) {
            printMemory("before forced gc");
            long gcStartMillis = System.currentTimeMillis();
            System.gc();
            long gcEndMillis = System.currentTimeMillis();
            printMemory("after forced gc");
            printElapsedTime("gc", gcStartMillis, gcEndMillis);
        }

        long getStartMillis = System.currentTimeMillis();
        int hashCode = map.entrySet().parallelStream()
                .mapToInt((x) -> x.getKey().hashCode() ^ x.getValue().hashCode())
                .reduce(0, (a, b) -> a ^ b);
        long getEndMillis = System.currentTimeMillis();
        printElapsedTime("get", getStartMillis, getEndMillis);
        printElapsedTime("all", putStartMillis, getEndMillis);
        printMemory("end");

        if (hashCode != expectedHashCode) {
            throw new RuntimeException("hashCode != expectedHashCode," +
                    " hashCode=" + hashCode + ", expectedHashCode=" + expectedHashCode);
        }
    }

    private static void printElapsedTime(String name, long startMillis, long endMillis) {
        float seconds = (float) (endMillis - startMillis) / 1000;
        System.out.println(name + " time: " + seconds + " seconds");
    }

    private static void printMemory(String description) {
        final int unit = 1048576;
        Runtime runtime = Runtime.getRuntime();
        float total = (float) runtime.totalMemory() / unit;
        float free = (float) runtime.freeMemory() / unit;
        float max = (float) runtime.maxMemory() / unit;
        System.out.println(description + " memory status (MiB): used=total-free=" + (total - free)
                + ", free=" + free + ", total=" + total + ", max=" + max);
    }
}
